# 跨域

# 同源策略

编程中的同源,比较的是两个 url 是否同源。

主要看下面三个方面:

  • 协议是否相同(http https file)
  • 主机地址是否相同(www.xxx.com 127.0.0.1)
  • 端口(0~65535)(http 默认端口是 80;https 默认端口是 443;MySQL 默认端口 3306)

如果两个 url 的协议、主机地址、端口都相同,那么这两个 url 是同源的,否则就是非同源。

image-20210126083715294

违反了同源策略的请求,叫做跨域请求

如果非同源,那么以下三种行为会受到限制:

  • Cookie 无法操作
  • DOM 无法操作
  • Ajax 请求无效(请求可以发送,服务器也会处理这次请求,但是响应结果会被浏览器拦截)

# 解决跨域

主流的方案有两种:分别是 JSONP 和 CORS.

# JSONP

是程序员被迫想出来的解决跨域的方案。

JSONP 方案和 Ajax 没有任何关系

JSONP 方案只支持 GET 请求

JSONP 没有浏览器兼容问题,任何浏览器都支持。

  • 原理

    • 客户端利用 script 标签的 src 属性,去请求一个接口,因为 src 属性不受跨域影响。
    • 服务端响应一个字符串
    • 客户端接收到字符串,然后把它当做 JS 代码运行。

    后端接口代码:

    app.get("/api/jsonp", (req, res) => {
      // res.send('hello');
      // res.send('console.log(1234)');
      // res.send('abc()')
      // res.send('abc(12345)')
    
      // 接收客户端的函数名
      let fn = req.query.callback;
      let obj = { status: 0, message: "登录成功" };
      let str = JSON.stringify(obj);
      res.send(fn + `(${str})`);
    });
    

    前端代码:

    <script>
      // 提前准备好一个函数
      function xxx(res) {
        console.log(res);
      }
    </script>
    
    <script src="http://localhost:3006/api/jsonp?callback=xxx"></script>
    
  • 前端需要做什么?

    • 如果使用 jQuery,$.ajax({ dataType: 'jsonp' }),必须指定 dataType 选项为 jsonp 即可
  • 后端需要做什么?

    • 如果使用 express,那么直接调用 res.jsonp(数据) 即可。

# CORS

由于 XHR 对象被 W3C 标准化之后,提出了很多 XHR Level2 的新构想,其中新增了很多新方法(onload、response....)和 CORS 跨域资源共享。浏览器升级后开始支持 CORS 方案,从 IE10 开始支持。

CORS 方案,就是通过服务器设置响应头来实现跨域

CORS 才是解决跨域的真正解决方案。

  • 前端需要做什么?
    • 无需做任何事情,正常发送 Ajax 请求即可。
  • 后端需要做什么?

# 小结

方案 前端 后端
CORS × 设置响应头
JSONP(原生) 1. 准备一个函数;2. 使用 script 的 src 发送请求 响应函数调用
JSONP(jQuery) 1. 还是调用$.ajax();2. 必须指定 dataType: 'jsonp' res.jsonp(数据)

# 防抖和节流

防抖和节流,作用类似,都是为了提高项目的性能。

# 防抖

当事件触发之后,约定单位时间(比如 1s)之后,执行里面的代码;如果在单位时间只内再次触发了事件,那么要重新计时,以保证事件里面的代码只执行一次

image-20210125164236632

image-20210126094601615

<style>
  * {
    margin: 0;
    padding: 0;
  }

  #box {
    width: 500px;
    margin: 20px auto;
  }

  ul,
  li {
    list-style: none;
  }

  input {
    width: 100%;
    height: 26px;
    line-height: 26px;
  }

  li:hover {
    background-color: beige;
  }

  ul {
    display: none;
  }
</style>

<div id="box">
  <input type="text" id="ipt" />
  <ul></ul>
</div>

<script src="./jquery.js"></script>
<script src="./template-web.js"></script>

<!-- 搜索建议模板 -->
<script type="text/html" id="tpl-list">
  {{each result item}}
    <li>{{item[0]}}</li>
  {{/each}}
</script>

<script>
  let timer = null;

  // 当输入框的键盘弹起的时候,发送请求,获取搜索建议
  $("#ipt").on("keyup", function() {
    // 清楚前面的定时器
    clearTimeout(timer);
    // 获取输入的值(搜索关键字)
    let keywords = $(this).val();
    if (keywords === "") {
      return $("ul")
        .empty()
        .hide();
    }

    // 如果关键字不为空,则获取搜索建议
    // 约定 1s 之后发送请求
    timer = setTimeout(() => {
      $.ajax({
        url: "https://suggest.taobao.com/sug",
        data: { q: keywords, code: "utf-8" }, // 加入code参数,能够搜索多个汉字
        dataType: "jsonp", // JSONP请求必须加这项
        success: function(res) {
          // console.log(res)
          let str = template("tpl-list", res);
          $("ul")
            .html(str)
            .show();
        },
      });
    }, 1000);
  });
</script>

# 节流

当事件触发之后,约定单位时间之内,事件里面的代码最多只能执行 1 次

所以,节流减少了单位时间内代码的执行次数,从而提高性能。

image-20210125164258892

image-20210126102632398

使用 timer 当做开关(节流阀)。

  • 开关 打开状态(timer = null),则允许执行代码。
  • 开关是关闭状态(timer = 数字),则不允许执行代码。

代码:

<style>
  html,
  body {
    height: 100%;
  }

  img {
    position: absolute;
  }
</style>

<img src="./angel.gif" alt="" />

<script src="./jquery.js"></script>
<script>
  let timer = null; // null,表示节流阀打开状态,允许执行事件里面的代码

  let img = $("img");
  $(document).on("mousemove", function(e) {
    //
    console.log(111);
    // 当事件触发了,判断一下,节流阀的状态,如果是关闭状态,则不允许创建另一个定时器
    if (timer !== null) return;

    timer = setTimeout(() => {
      console.log(222);
      let x = e.pageX;
      let y = e.pageY;
      // 设置图片的css(left和top)
      img.css({ left: x + "px", top: y + "px" });
      // 当定时器执行完毕,重新打开节流阀
      timer = null;
    }, 16);
  });
</script>

# ES6 降级处理

因为 ES 6 有浏览器兼容性问题,可以使用一些工具进行降级处理,例如:babel

  • 降级处理 babel 的使用步骤

    1. 安装 Node.js
    2. 命令行中安装 babel
    3. 配置文件 .babelrc
    4. 运行
  • 项目初始化 (项目文件夹不能有中文)

    npm init -y
    
  • 在命令行中,安装 babel babel 官网 (opens new window)

    npm install  @babel/core @babel/cli @babel/preset-env
    
  • 配置文件 .babelrc (手工创建这个文件)

    babel 的降级处理配置

    {
      "presets": ["@babel/preset-env"]
    }
    
  • 在命令行中,运行

    # 把转换的结果输出到指定的文件
    npx babel index.js -o test.js
    # 把转换的结果输出到指定的目录
    npx babel 包含有js的原目录 -d 转换后的新目录
    

# Promise

Promise 能够处理异步程序。

# 回调地狱

image-2019114115722

JS 中或 node 中,都大量的使用了回调函数进行异步操作,而异步操作什么时候返回结果是不可控的,如果我们希望几个异步请求按照顺序来执行,那么就需要将这些异步操作嵌套起来,嵌套的层数特别多,就会形成回调地狱 或者叫做 横向金字塔

下面的案例就有回调地狱的意思:

案例:有 a.txt、b.txt、c.txt 三个文件,使用 fs 模板按照顺序来读取里面的内容,代码:

// 将读取的a、b、c里面的内容,按照顺序输出
const fs = require("fs");

// 读取a文件
fs.readFile("./a.txt", "utf-8", (err, data) => {
  if (err) throw err;
  console.log(data.length);
  // 读取b文件
  fs.readFile("./b.txt", "utf-8", (err, data) => {
    if (err) throw err;
    console.log(data);
    // 读取c文件
    fs.readFile("./c.txt", "utf-8", (err, data) => {
      if (err) throw err;
      console.log(data);
    });
  });
});

案例中,只有三个文件,试想如果需要按照顺序读取的文件非常多,那么嵌套的代码将会多的可怕,这就是回调地狱的意思。

# Promise 简介

  • Promise 对象可以解决回调地狱的问题
  • Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
  • Promise可以理解为一个容器,里面可以编写异步程序的代码
  • 从语法上说,Promise 是一个对象,使用的使用需要 new

# Promise 简单使用

Promise 是“承诺”的意思,实例中,它里面的异步操作就相当于一个承诺,而承诺就会有两种结果,要么完成了承诺的内容,要么失败。

所以,使用 Promise,分为两大部分,首先是有一个承诺(异步操作),然后再兑现结果。

第一部分:定义“承诺”

// 实例化一个Promise,表示定义一个容器,需要给它传递一个函数作为参数,而该函数又有两个形参,通常用resolve和reject来表示。该函数里面可以写异步请求的代码
// 换个角度,也可以理解为定下了一个承诺
let p = new Promise((resolve, reject) => {
  // 形参resolve,单词意思是 完成
  // 形参reject ,单词意思是 失败
  fs.readFile("./a.txt", "utf-8", (err, data) => {
    if (err) {
      // 失败,就告诉别人,承诺失败了
      reject(err);
    } else {
      // 成功,就告诉别人,承诺实现了
      resolve(data.length);
    }
  });
});

第二部分:获取“承诺”的结果

// 通过调用 p 的then方法,可以获取到上述 “承诺” 的结果
// then方法有两个函数类型的参数,参数1表示承诺成功时调用的函数,参数2可选,表示承诺失败时执行的函数
p.then(
  (data) => {},
  (err) => {}
);

完整的代码:

const fs = require("fs");
// promise 承诺

// 使用Promise分为两大部分

// 1. 定义一个承诺
let p = new Promise((resolve, reject) => {
  // resolve -- 解决,完成了; 是一个函数
  // reject  -- 拒绝,失败了; 是一个函数
  // 异步操作的代码,它就是一个承诺
  fs.readFile("./a.txt", "utf-8", (err, data) => {
    if (err) {
      reject(err);
    } else {
      resolve(data.length);
    }
  });
});

// 2. 兑现承诺
// p.then(
//     (data) => {}, // 函数类似的参数,用于获取承诺成功后的数据
//     (err) => {} // 函数类型的参数,用于或承诺失败后的错误信息
// );
p.then(
  (data) => {
    console.log(data);
  },
  (err) => {
    console.log(err);
  }
);

# then 方法的链式调用

  • 前一个 then 里面返回的字符串,会被下一个 then 方法接收到。但是没有意义;

  • 前一个 then 里面返回的 Promise 对象,并且调用 resolve 的时候传递了数据,数据会被下一个 then 接收到

  • 前一个 then 里面如果没有调用 resolve,则后续的 then 不会接收到任何值

    const fs = require("fs");
    // promise 承诺
    
    new Promise((resolve, reject) => {
      fs.readFile("./a.txt", "utf-8", (err, data) => {
        err ? reject(err) : resolve(data.length);
      });
    })
      .then((a) => {
        console.log(a);
        return new Promise((resolve, reject) => {
          fs.readFile("./a.txt", "utf-8", (err, data) => {
            err ? reject(err) : resolve(data.length);
          });
        });
      })
      .then((b) => {
        console.log(b);
        return new Promise((resolve, reject) => {
          fs.readFile("./a.txt", "utf-8", (err, data) => {
            err ? reject(err) : resolve(data.length);
          });
        });
      })
      .then((c) => {
        console.log(c);
      })
      .catch((err) => {
        console.log(err);
      });
    

    catch 方法可以统一获取错误信息

# 封装按顺序异步读取文件的函数

function myReadFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, "utf-8", (err, data) => {
      err ? reject(err) : resolve(data.length);
    });
  });
}

myReadFile("./a.txt")
  .then((a) => {
    console.log(a);
    return myReadFile("./b.txt");
  })
  .then((b) => {
    console.log(b);
    return myReadFile("./c.txt");
  })
  .then((c) => {
    console.log(c);
  })
  .catch((err) => {
    console.log(err);
  });

# async 和 await 修饰符

ES6 --- ES2015

async 和 await 是 ES2017 中提出来的。

异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。

从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。

异步 I/O 不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步。

==async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案==。

ES2017 提供了 async 和 await 关键字。await 和 async 关键词能够将异步请求的结果以返回值的方式返回给我们。

  • async 用于修饰一个 function
  • async 修饰的函数,一般表示该函数里面有异步操作(Promise 的调用)
  • await和async需要配合使用,没有async修饰的函数中使用await是没有意义的,会报错
  • await 需要定义在 async 函数内部,await 后面跟的一般都是一个函数(函数里面包含有 Promise)的调用
  • await 修饰的异步操作,可以使用返回值的方式去接收异步操作的结果
  • 如果有哪一个 await 操作出错了,会中断 async 函数的执行

总结来说:async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果

const fs = require("fs");
// 将异步读取文件的代码封装
function myReadFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, "utf-8", (err, data) => {
      err ? reject(err) : resolve(data.length);
    });
  }).catch((err) => {
    console.log(err);
  });
}

async function abc() {
  let a = await myReadFile("./a.txt");
  let b = await myReadFile("./b.txt");
  let c = await myReadFile("./c.txt");
  console.log(b);
  console.log(a);
  console.log(c);
}

abc();

# Promise 小结

  1. 封装函数
    • 函数中返回 Promise 对象
      • Promise 对象里面写你的异步代码
  2. 使用 async 修改一个函数
    • 函数里面使用 await 修饰 Promise 对象(调用前面封装的函数)
    • 以返回值的方式获取异步代码成功的结果了

# 身份认证(了解)

# 开发模式

  • 传统的服务端渲染模式
    • 后端的接口和前端的代码在一起(服务器)
    • 涉及不到跨域
    • 有利于 SEO
    • 客户端(前端)不需要渲染数据,如果是手机,将会非常省电。运行速度非常快。
    • 缺点是开发效率低。
    • 适合使用 cookiesession 身份认证
  • 新型的前后端分离模式
    • 前端代码单独在一个文件夹(服务器)(自己的电脑上)
    • 后端的接口在另外的文件夹(服务器)(刘龙宾老师的服务器上)
    • 开发速度快,适合多人协作开发。
    • 适合使用 JWT(json web token) 方式的身份认证。

PS:一个项目到底该使用哪种开发模式?

  • 不能一概而论,比如有的网站,首页为了 SEO 采用传统的服务端渲染模式,其他页面采用前后端分离模式。
  • 后台管理系统,涉及不到 SEO,可以采用前后端分离模式。
  • 小型企业网站,可以采用传统的服务端渲染模式。

# 演示传统的服务端渲染模式

  • 最大的特点

  • 服务端代码和前端代码在同一个服务器(文件夹)

  • 搭建服务器

    | - app.js               (搭建服务器)
    | - public               (public文件夹用于存放前端页面)
    	| - index.html       (一个前端的html页面)
    
  • app.js 编写接口 /index.html

    • 接口中,使用 fs 读取文件,并替换内容,最后响应给客户端
    • 客户端请求 http://localhost:3006/index.html
// 接口,提供index.html 页面
app.get("/index.html", (req, res) => {
  // 客户端发来请求,希望看到index.html 页面。
  // 服务器,把html页面读取出来,把读取的结果响应给客户端即可
  fs.readFile("./public/index.html", "utf-8", (err, data) => {
    if (err) throw err;
    // console.log(data);
    // 假设从数据库中查询到了标题和内容
    data = data.replace("{{title}}", "咏鹅");
    data = data.replace("{{content}}", "鹅鹅鹅,曲项向天歌");
    res.send(data);
  });
});

页面中的数据,是在服务端完成渲染的,客户端接收到的已经是一个包含数据的完整页面了,所以叫做服务端渲染模式。

# 原理图

身份认证,要完成的是:不登录,不允许访问其他页面。

image-20201014035909

# 实现身份认证

  • 搭建基础的服务器(或者直接使用前面的 传统服务端渲染模式 代码)
  • 中间件配置 cookie-parser
    • app.use(cookieParser())
  • 模拟一个登录接口
    • 如果登录成功,设置 cookie。res.cookie('key', 'value', 配置项);
  • /index.html 接口中,根据 cookie 判断是否登录,从而完成身份认证

详见代码

# 优缺点

  • 优点
    • 体积小
    • 客户端存放,不占用服务器空间
    • 浏览器会自动携带,不需要写额外的代码,比较方便
  • 缺点
    • 客户端保存,安全性较低。但可以存放加密的字符串来解决
    • 只能存字符串,cookie 的大小也是有限制的
    • 可以实现跨域,但是难度大,难理解,代码难度高
    • 不适合前后端分离式的开发

# 适用场景

  • 传统的服务器渲染模式
  • 存储安全性较低的数据,比如视频播放位置等

# Session 身份认证

# 原理图

要实现的效果:不登录,不允许访问其他接口

image-202010141403755

# 实现身份认证

  • 搭建基础的服务器

    • 下载安装第三方模块 expressexpress-session
    • 创建 app.js
    • 加载所需模块
      • const express = require('express');
      • const session = require('express-session');
  • 中间件配置 session

    app.use(
      session({
        secret: "adfasdf", // 这个随便写
        saveUninitialized: false,
        resave: false,
      })
    );
    
  • 完成登录接口

    • 如果登录成功,使用 session 记录用户信息。

      req.session.isLogin = true;
      req.session.username = "laotang";
      
  • /index.html 接口中,根据 session 判断是否登录,从而完成身份认证

详见代码

# 优缺点

  • 优点
    • 服务端存放,安全性较高
    • 浏览器会自动携带 cookie,不需要写额外的代码,比较方便
    • 适合服务器端渲染模式
  • 缺点
    • 会占用服务器端空间
    • session 实现离不开 cookie,如果浏览器禁用 cookie,session 不好实现
    • 不适合前后端分离式的开发

# 适用场景

  • 传统的服务器渲染模式
  • 安全性要求较高的数据可以使用 session 存放,比如用户私密信息、验证码等